﻿namespace System.Web.Mvc.Html5
{
    using System.ComponentModel.DataAnnotations;
    using System.Linq;

    using Collections.Generic;
    using Globalization;
    using Linq.Expressions;
    using Routing;

    /// <summary>
    /// Html5 Input Types
    /// </summary>
    public static class InputTypes
    {
        private static readonly Utilities Utilities = new Utilities();

        #region InputTypes

        /// <summary>
        /// The input type.
        /// </summary>
        public enum InputType
        {
            /// <summary>
            /// Text Input Type
            /// </summary>
            Text,

            /// <summary>
            /// Color Input Type
            /// </summary>
            Color,

            /// <summary>
            /// Email Input Type
            /// </summary>
            Email,

            /// <summary>
            /// Search Input Type
            /// </summary>
            Search,

            /// <summary>
            /// Time Input Type
            /// </summary>
            Time,

            /// <summary>
            /// Telephone Input Type
            /// </summary>
            Tel,

            /// <summary>
            /// File Input Type
            /// </summary>
            File,

            /// <summary>
            /// Date Input Type
            /// </summary>
            Date,

            /// <summary>
            /// Date Time Input Type
            /// </summary>
            Datetime,

            /// <summary>
            /// Month Input Type
            /// </summary>
            Month,

            /// <summary>
            /// Week Input Type
            /// </summary>
            Week,

            /// <summary>
            /// URL Input Type
            /// </summary>
            Url,

            /// <summary>
            /// Password Input Type
            /// </summary>
            Password,

            /// <summary>
            /// Number Input Type
            /// </summary>
            Number
        }

        /// <summary>
        /// The range number type.
        /// </summary>
        public enum RangeNumberType
        {
            /// <summary>
            /// Range Type
            /// </summary>
            Range,

            /// <summary>
            /// Number Type
            /// </summary>
            Number
        }
        #endregion

        /// <summary>
        /// Returns an HTML5 textbox element
        /// </summary>
        /// <param name="helper">The HTML helper.</param>
        /// <param name="name">The name of the input</param>
        /// <param name="inputType">Type of the input.</param>
        /// <param name="htmlAttributes">The HTML attributes.</param>
        /// <param name="placeHolder">The place holder.</param>
        /// <param name="isReadOnly">Is the textbox readonly</param>
        /// <param name="disabled">Is the textbox disabled</param>
        /// <param name="autoFocus">Autofocus on the textbox</param>
        /// <returns>An MVC HTML String</returns>
        public static MvcHtmlString Html5TextBox(this HtmlHelper helper, string name, InputType inputType = InputType.Text, object htmlAttributes = null, string placeHolder = null, bool isReadOnly = false, bool disabled = false, bool autoFocus = false)
        {
            var builder = BuildInputType(name, inputType.ToString());

            return BuildHtmlAttributes(htmlAttributes, placeHolder, isReadOnly, disabled, autoFocus, builder);
        }

        /// <summary>
        /// Builds an Html5 input type of range (slider) / Number.
        /// </summary>
        /// <param name="helper">The HTML helper.</param>
        /// <param name="minValue">The min value.</param>
        /// <param name="maxValue">The max value.</param>
        /// <param name="step">The step. Used for HTML5 range.</param>
        /// <param name="defaultValue">The default value.</param>
        /// <param name="htmlAttributes">The HTML attributes.</param>
        /// <param name="isReadOnly">Is element read only</param>
        /// <param name="disabled">Is the element disabled</param>
        /// <param name="autoFocus">Autofocus on the element</param>
        /// <returns>An MVC HTML string.</returns>
        public static MvcHtmlString Html5Number(this HtmlHelper helper, int minValue, int maxValue, int step, int defaultValue, object htmlAttributes, bool isReadOnly = false, bool disabled = false, bool autoFocus = false)
        {
            return Html5RangeNumber(
                helper,
                "number",
                minValue,
                maxValue,
                step,
                defaultValue,
                htmlAttributes,
                isReadOnly,
                disabled,
                autoFocus);
        }

        /// <summary>
        /// Builds an Html5 input type of range (slider)
        /// </summary>
        /// <param name="helper">The HTML helper.</param>
        /// <param name="minValue">The min value.</param>
        /// <param name="maxValue">The max value.</param>
        /// <param name="step">The step. Used for HTML5 Range.</param>
        /// <param name="defaultValue">The default value.</param>
        /// <param name="htmlAttributes">The HTML attributes.</param>
        /// <param name="isReadOnly">Is element read only</param>
        /// <param name="disabled">Is the element disabled</param>
        /// <param name="autoFocus">Autofocus on the element</param>
        /// <returns>An MVC HTML String.</returns>
        public static MvcHtmlString Html5Range(this HtmlHelper helper, int minValue, int maxValue, int? step, int defaultValue, object htmlAttributes, bool isReadOnly = false, bool disabled = false, bool autoFocus = false)
        {
            return Html5RangeNumber(helper,
                                    "range",
                                    minValue,
                                    maxValue,
                                    step,
                                    defaultValue,
                                    htmlAttributes,
                                    isReadOnly,
                                    disabled,
                                    autoFocus);
        }

        /// <summary>
        /// Returns an HTML5 textbox element for each property in the object that is represented by the expression parameter.
        /// </summary>
        /// <typeparam name="TModel">The type of the model.</typeparam>
        /// <typeparam name="TProperty">The type of the property.</typeparam>
        /// <param name="htmlHelper">The HTML helper.</param>
        /// <param name="expression">The expression.</param>
        /// <param name="inputType">Type of the input.</param>
        /// <param name="htmlAttributes">The HTML attributes</param>
        /// <param name="placeHolder">The HTML5 placeholder text to use in the input type.</param>
        /// <param name="isReadOnly">Is element read only</param>
        /// <param name="disabled">Is the element disabled</param>
        /// <param name="autoFocus">Autofocus on the element</param>
        /// <returns>
        /// HTML5 textbox element
        /// </returns>
        public static MvcHtmlString Html5TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, InputType? inputType = null, object htmlAttributes = null, string placeHolder = null, bool isReadOnly = false, bool disabled = false, bool autoFocus = false)
        {
            // Check the HTML attributes
            var modelMetadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);

            string modelName = modelMetadata.ContainerType.FullName;

            // Get the validation metadata
            FieldValidationMetadata fieldMetadata = GetFieldValidationMetadata(htmlHelper, modelMetadata, modelName);
            var routeHtmlAttributes = AddValidationHtmlAttributes(htmlAttributes, fieldMetadata);

            // Detect the input type if one isnt passed through
            InputType newInputType = inputType == null ? TryToFindInputType(modelMetadata) : (InputType)inputType;

            return htmlHelper.Html5TextBoxForHelper(expression, newInputType, routeHtmlAttributes, placeHolder, isReadOnly, disabled, autoFocus);
        }

        /// <summary>
        /// Gets the model state value.
        /// </summary>
        /// <param name="htmlHelper">The HTML helper.</param>
        /// <param name="key">The key of the model.</param>
        /// <param name="destinationType">Type of the destination.</param>
        /// <returns>The Model State object.</returns>
        public static object GetModelStateValue(this HtmlHelper htmlHelper, string key, Type destinationType)
        {
            ModelState state;
            if (htmlHelper.ViewData.ModelState.TryGetValue(key, out state) && (state.Value != null))
            {
                return state.Value.ConvertTo(destinationType, null);
            }

            return null;
        }

        /// <summary>
        /// Returns an HTML5 editor-textbox element (eg textbox with validation attributes)
        /// </summary>
        /// <param name="htmlHelper">The HTML helper.</param>
        /// <param name="name">The name of the input</param>
        /// <param name="inputType">Type of the input.</param>
        /// <param name="htmlAttributes">The HTML attributes.</param>
        /// <param name="placeHolder">The place holder.</param>
        /// <param name="isReadOnly">Is the textbox readonly</param>
        /// <param name="disabled">Is the textbox disabled</param>
        /// <param name="autoFocus">Autofocus on the textbox</param>
        /// <returns>An MVC HTML String</returns>
        public static MvcHtmlString Html5Editor(this HtmlHelper htmlHelper, string name, InputType inputType, object htmlAttributes = null, string placeHolder = null, bool isReadOnly = false, bool disabled = false, bool autoFocus = false)
        {
            return htmlHelper.Html5TextBox(name, inputType, htmlAttributes, placeHolder, isReadOnly, disabled, autoFocus);
        }

        /// <summary>
        /// Returns an HTML5 editor-textbox element (eg textbox with validation attributes)
        /// </summary>
        /// <typeparam name="TModel">The type of the model.</typeparam>
        /// <typeparam name="TProperty">The type of the property.</typeparam>
        /// <param name="htmlHelper">The HTML helper.</param>
        /// <param name="expression">The expression.</param>
        /// <param name="inputType">Type of the input.</param>
        /// <param name="htmlAttributes">The HTML attributes.</param>
        /// <param name="placeHolder">The place holder.</param>
        /// <param name="isReadOnly">Is the textbox readonly</param>
        /// <param name="disabled">Is the textbox disabled</param>
        /// <param name="autoFocus">Autofocus on the textbox</param>
        /// <returns>An MVC HTML String</returns>
        public static MvcHtmlString Html5EditorFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, InputType? inputType = null, object htmlAttributes = null, string placeHolder = null, bool isReadOnly = false, bool disabled = false, bool autoFocus = false)
        {
            return htmlHelper.Html5TextBoxFor(expression, inputType, htmlAttributes, placeHolder, isReadOnly, disabled, autoFocus);
        }

        /// <summary>
        /// Builds an Html5 input type of number.
        /// </summary>
        /// <param name="helper">The HTML helper.</param>
        /// <param name="type">The input type</param>
        /// <param name="minValue">The min value.</param>
        /// <param name="maxValue">The max value.</param>
        /// <param name="step">The step. Used for HTML5 range.</param>
        /// <param name="defaultValue">The default value.</param>
        /// <param name="htmlAttributes">The HTML attributes.</param>
        /// <param name="isReadOnly">Is element read only</param>
        /// <param name="disabled">Is the element disabled</param>
        /// <param name="autoFocus">Autofocus on the element</param>
        /// <returns>HTML5 textbox element</returns>
        private static MvcHtmlString Html5RangeNumber(this HtmlHelper helper, string type, int minValue, int maxValue, int? step, int defaultValue, object htmlAttributes, bool isReadOnly = false, bool disabled = false, bool autoFocus = false)
        {
            TagBuilder builder = new TagBuilder("input");
            builder.MergeAttribute("type", type);
            builder.MergeAttribute("min", minValue.ToString());
            builder.MergeAttribute("max", maxValue.ToString());

            if (step != null)
            {
                builder.MergeAttribute("step", step.ToString());
            }

            builder.MergeAttribute("value", defaultValue.ToString());

            // Add any html attrributes.
            if (htmlAttributes != null)
            {
                builder.MergeAttributes(htmlAttributes as IDictionary<string, object> ?? new RouteValueDictionary(htmlAttributes));
            }

            // Add the single HTML5 attributes
            IEnumerable<string> singleAttributes = Utilities.MergeSingleAttribute(isReadOnly, disabled, false, autoFocus);

            string originalTag = builder.ToString(TagRenderMode.SelfClosing);
            string updatedTag = originalTag.Replace(Utilities.GetAttributesString(builder),
                                                    Utilities.AddSingleAttributes(builder,
                                                    singleAttributes));

            return Extensions.ToHtmlString(updatedTag);
        }

        /// <summary>
        /// Returns an HTML5 textbox element for each property in the object that is represented by the expression parameter.
        /// </summary>
        /// <typeparam name="T">The input type.</typeparam>
        /// <typeparam name="T1">The type of the 1.</typeparam>
        /// <param name="htmlHelper">The HTML helper.</param>
        /// <param name="expression">The expression.</param>
        /// <param name="inputType">Type of the input.</param>
        /// <param name="htmlAttributes">The HTML attributes.</param>
        /// <param name="placeholder">The placeholder.</param>
        /// <param name="isReadOnly">Is element read only</param>
        /// <param name="disabled">Is the element disabled</param>
        /// <param name="autoFocus">Autofocus on the element</param>
        /// <returns>HTML5 textbox element</returns>
        private static MvcHtmlString Html5TextBoxForHelper<T, T1>(this HtmlHelper<T> htmlHelper, Expression<Func<T, T1>> expression, InputType inputType, object htmlAttributes, string placeholder, bool isReadOnly = false, bool disabled = false, bool autoFocus = false)
        {
            ModelMetadata modelMetadata = ModelMetadata.FromLambdaExpression<T, T1>(expression, htmlHelper.ViewData);

            string expressionText = ExpressionHelper.GetExpressionText(expression);

            return InputHelper(htmlHelper, expressionText, inputType, modelMetadata.Model, htmlAttributes,
                               placeholder ?? modelMetadata.DisplayName, isReadOnly, disabled, autoFocus);
        }

        /// <summary>
        /// Builds the HTML attributes.
        /// </summary>
        /// <param name="htmlAttributes">
        /// The HTML attributes.
        /// </param>
        /// <param name="placeHolder">
        /// The place holder.
        /// </param>
        /// <param name="isReadOnly">
        /// If set to <c>true</c> [is read only].
        /// </param>
        /// <param name="disabled">
        /// If set to <c>true</c> [disabled].
        /// </param>
        /// <param name="autoFocus">
        /// If set to <c>true</c> [auto focus].
        /// </param>
        /// <param name="builder">
        /// The builder.
        /// </param>
        /// <returns>
        /// An MVC HTML5 string.
        /// </returns>
        private static MvcHtmlString BuildHtmlAttributes(object htmlAttributes, string placeHolder, bool isReadOnly, bool disabled, bool autoFocus, TagBuilder builder)
        {
            // Add the html attributes
            if (htmlAttributes != null)
            {
                builder.MergeAttributes(htmlAttributes as IDictionary<string, object> ?? new RouteValueDictionary(htmlAttributes));
            }

            if (!string.IsNullOrWhiteSpace(placeHolder))
            {
                builder.MergeAttribute("placeholder", placeHolder);
            }

            // Add the single HTML5 attributes
            IEnumerable<string> singleAttributes = Utilities.MergeSingleAttribute(isReadOnly, disabled, false, autoFocus);

            string originalTag = builder.ToString(TagRenderMode.SelfClosing);
            string updatedTag = originalTag.Replace(Utilities.GetAttributesString(builder),
                                                    Utilities.AddSingleAttributes(builder,
                                                                                  singleAttributes));

            return Extensions.ToHtmlString(updatedTag);
        }

        /// <summary>
        /// Builds the type of the input using an MVC TagBuilder.
        /// </summary>
        /// <param name="name">The name of the Input type.</param>
        /// <param name="inputType">Type of the input.</param>
        /// <returns>A Tagbuilder.</returns>
        private static TagBuilder BuildInputType(string name, string inputType)
        {
            TagBuilder builder = new TagBuilder("input");
            builder.MergeAttribute("type", inputType);
            if (name != null)
            {
                builder.MergeAttribute("name", name);
            }

            return builder;
        }

        /// <summary>
        /// Input helper for the Html5 textboxes.
        /// </summary>
        /// <param name="htmlHelper">The HTML helper.</param>
        /// <param name="name">The name of the input type.</param>
        /// <param name="inputType">Type of the input.</param>
        /// <param name="value">The value of the input.</param>
        /// <param name="htmlAttributes">The HTML attributes.</param>
        /// <param name="placeholder">The placeholder.</param>
        /// <param name="isReadOnly">Is element read only</param>
        /// <param name="disabled">Is the element disabled</param>
        /// <param name="autoFocus">Autofocus on the element</param>
        /// <returns>HTML5 textbox element</returns>
        private static MvcHtmlString InputHelper(this HtmlHelper htmlHelper, string name, InputType inputType, object value, object htmlAttributes, string placeholder, bool isReadOnly = false, bool disabled = false, bool autoFocus = false)
        {
            ModelState state;
            TagBuilder builder = new TagBuilder("input");

            var attributes = htmlAttributes as IDictionary<string, object> ?? new RouteValueDictionary(htmlAttributes);
            builder.MergeAttributes(attributes);
            builder.MergeAttribute("type", inputType.ToString().ToLowerInvariant());
            builder.MergeAttribute("name", name, true);
            if (placeholder != null)
            {
                builder.MergeAttribute("placeholder", placeholder);
            }

            string handedValue = Convert.ToString(value, CultureInfo.CurrentCulture);

            // Get the model state value
            // Don't return the value if it is a password input type
            if (inputType == InputType.Password)
            {
                builder.MergeAttribute("value", string.Empty);
            }
            else
            {
                string retrieveValue = (string)htmlHelper.GetModelStateValue(name, typeof(string));
                builder.MergeAttribute("value", retrieveValue ?? handedValue);
            }

            builder.GenerateId(name);

            // Loop through the html attributes
            if (htmlAttributes != null)
            {
                foreach (var attribute in attributes)
                {
                    builder.MergeAttribute(attribute.Key, attribute.Value.ToString(), true);
                }
            }

            // Check if the model is valid, else display validation css.
            if (htmlHelper.ViewData.ModelState.TryGetValue(name, out state) && (state.Errors.Count > 0))
            {
                builder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
            }

            // Add the single HTML5 attributes
            IEnumerable<string> singleAttributes = Utilities.MergeSingleAttribute(isReadOnly, disabled, false, autoFocus);

            string originalTag = builder.ToString(TagRenderMode.SelfClosing);
            string updatedTag = originalTag.Replace(Utilities.GetAttributesString(builder),
                                                    Utilities.AddSingleAttributes(builder, singleAttributes));

            return Extensions.ToHtmlString(updatedTag);
        }

        /// <summary>
        /// Use the model metadata and try and find out what the input types are.
        /// </summary>
        /// <param name="modelMetadata">The model meta data.</param>
        /// <returns>An input type based on the model metadata.</returns>
        private static InputType TryToFindInputType(ModelMetadata modelMetadata)
        {
                if (modelMetadata.DataTypeName == null)
                {
                    if (!modelMetadata.ModelType.IsClass)
                    {
                        string typeString = modelMetadata.ModelType.ToString().ToLowerInvariant();
                        switch (typeString)
                        {
                            case "date":
                                return InputType.Date;
                            case "imageurl":
                                return InputType.Url;
                            case "phone":
                            case "phonenumber":
                                return InputType.Tel;
                            case "string":
                                return InputType.Text;
                            case "timespan":
                                return InputType.Time;
                            case "uri":
                                return InputType.Url;
                            case "int":
                            case "long":
                            case "short":
                                return InputType.Number;
                            case "color":
                                return InputType.Color;
                            default:
                                InputType inputTypeValue;
                                if (Enum.TryParse(typeString, out inputTypeValue))
                                {
                                    return inputTypeValue;
                                }

                                break;
                        }
                    }
                }
                else
                {
                    DataType dataTypeValue;
                    if (Enum.TryParse(modelMetadata.DataTypeName, true, out dataTypeValue))
                    {
                        switch (dataTypeValue)
                        {
                            case DataType.Date:
                                return InputType.Date;
                            case DataType.DateTime:
                                return InputType.Datetime;
                            case DataType.EmailAddress:
                                return InputType.Email;
                            case DataType.ImageUrl:
                                return InputType.Url;
                            case DataType.Password:
                                return InputType.Password;
                            case DataType.PhoneNumber:
                                return InputType.Tel;
                            case DataType.Text:
                                return InputType.Text;
                            case DataType.Time:
                                return InputType.Time;
                            case DataType.Url:
                                return InputType.Url;
                        }
                    }

                    InputType inputTypeValue;
                    if (Enum.TryParse(modelMetadata.DataTypeName, out inputTypeValue))
                    {
                        return inputTypeValue;
                    }
                }

            // If we can't decide which one, then simply return text.
            return InputType.Text;
        }

        /// <summary>
        /// Adds validation attributes to the HTML.
        /// </summary>
        /// <param name="htmlAttributes">The HTML attributes.</param>
        /// <param name="fieldMetadata">The field meta data.</param>
        /// <returns>A dictionary containing the HTML attributes.</returns>
        private static IDictionary<string, object> AddValidationHtmlAttributes(object htmlAttributes, FieldValidationMetadata fieldMetadata)
        {
            var routeHtmlAttributes = htmlAttributes as IDictionary<string, object> ?? new RouteValueDictionary(htmlAttributes);
            if (fieldMetadata.ValidationRules.Count > 0)
            {
                routeHtmlAttributes.Add("data-val", "true");
                foreach (var item in fieldMetadata.ValidationRules)
                {
                    routeHtmlAttributes.Add(string.Format("data-val-{0}", item.ValidationType), item.ErrorMessage);
                    foreach (var attrib in item.ValidationParameters)
                    {
                        routeHtmlAttributes.Add(string.Format("data-val-{0}-{1}", item.ValidationType, attrib.Key), attrib.Value);
                    }
                }
            }

            return routeHtmlAttributes;
        }

        /// <summary>
        /// Gets the validation data for the fields as set by the data annotations.
        /// </summary>
        /// <param name="htmlHelper">The HTML Helper.</param>
        /// <param name="modelMetadata">The model meta data.</param>
        /// <param name="modelName">The model name.</param>
        /// <returns>A FieldValidationMetadata object.</returns>
        private static FieldValidationMetadata GetFieldValidationMetadata(HtmlHelper htmlHelper, ModelMetadata modelMetadata, string modelName)
        {
            FormContext formContext = htmlHelper.ViewContext.FormContext;
            FieldValidationMetadata fieldMetadata = formContext.GetValidationMetadataForField(modelName, true /* createIfNotFound */);

            if (fieldMetadata.ValidationRules.Count == 0)
            {
                // write rules to context object
                IEnumerable<ModelValidator> validators = ModelValidatorProviders.Providers.GetValidators(modelMetadata, htmlHelper.ViewContext);
                foreach (ModelClientValidationRule rule in validators.SelectMany(v => v.GetClientValidationRules()))
                {
                    fieldMetadata.ValidationRules.Add(rule);
                }
            }

            return fieldMetadata;
        }
    }
}
